package com.steakliu.sun.spi.loader;

import com.steakliu.sun.spi.annotation.Join;
import com.steakliu.sun.spi.annotation.SPI;
import com.steakliu.sun.spi.factory.ExtensionFactory;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

/**
 * The type Extension loader.
 * This is done by loading the properties file.
 *
 * @param <T> the type parameter
 * @see <a href="https://github.com/apache/dubbo/blob/master/dubbo-common/src/main/java/org/apache/dubbo/common/extension/ExtensionLoader.java">ExtensionLoader</a>
 */
public final class ExtensionLoader<T> {

  private static final Logger LOG = LoggerFactory.getLogger(ExtensionLoader.class);

  private static final String SERVICES_DIRECTORY = "META-INF/services/";
  private static final String STEAKLIU_DIRECTORY = "META-INF/steakliu/";
  private static final String STEAKLIU_DIRECTORY_EXTERNAL = "META-INF/steakliu/external/";
  private static final String STEAKLIU_DIRECTORY_INTERNAL = "META-INF/steakliu/internal/";

  private static final String[] SPI_DIRECTORIES = new String[]{
    SERVICES_DIRECTORY,
    STEAKLIU_DIRECTORY,
    STEAKLIU_DIRECTORY_EXTERNAL,
    STEAKLIU_DIRECTORY_INTERNAL
  };

  private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();

  private final Class<T> clazz;

  private final ClassLoader classLoader;

  private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();

  private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();

  private final Map<Class<?>, Object> spiClassInstances = new ConcurrentHashMap<>();

  private String cachedDefaultName;

  /**
   * Instantiates a new Extension loader.
   *
   * @param clazz the clazz.
   */
  private ExtensionLoader(final Class<T> clazz, final ClassLoader cl) {
    this.clazz = clazz;
    this.classLoader = cl;
    if (!Objects.equals(clazz, ExtensionFactory.class)) {
      ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getExtensionClasses();
    }
  }

  /**
   * Gets extension loader.
   *
   * @param <T>   the type parameter
   * @param clazz the clazz
   * @param cl    the cl
   * @return the extension loader.
   */
  public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz, final ClassLoader cl) {

    Objects.requireNonNull(clazz, "extension clazz is null");

    if (!clazz.isInterface()) {
      throw new IllegalArgumentException("extension clazz (" + clazz + ") is not interface!");
    }
    if (!clazz.isAnnotationPresent(SPI.class)) {
      throw new IllegalArgumentException("extension clazz (" + clazz + ") without @" + SPI.class + " Annotation");
    }
    ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) LOADERS.get(clazz);
    if (Objects.nonNull(extensionLoader)) {
      return extensionLoader;
    }
    LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz, cl));
    return (ExtensionLoader<T>) LOADERS.get(clazz);
  }


  /**
   * 直接获取想要的类实例
   *
   * @param clazz 接口的Class实例
   * @param name  SPI名称
   * @param <T>   泛型类型
   * @return 泛型实例
   */
  public static <T> T getExtension(final Class<T> clazz, String name) {
    return StringUtils.isEmpty(name) ? getExtensionLoader(clazz).getDefaultSpiClassInstance() : getExtensionLoader(clazz).getSpiClassInstance(name);
  }

  /**
   * Gets extension loader.
   *
   * @param <T>   the type parameter
   * @param clazz the clazz
   * @return the extension loader
   */
  public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
    return getExtensionLoader(clazz, ExtensionLoader.class.getClassLoader());
  }

  /**
   * Gets default spi class instance.
   *
   * @return the default spi class instance.
   */
  public T getDefaultSpiClassInstance() {
    getExtensionClasses();
    if (StringUtils.isBlank(cachedDefaultName)) {
      return null;
    }
    return getSpiClassInstance(cachedDefaultName);
  }

  /**
   * Gets spi class.
   *
   * @param name the name
   * @return the spi class instance.
   */
  public T getSpiClassInstance(final String name) {
    if (StringUtils.isBlank(name)) {
      throw new NullPointerException("get spi class name is null");
    }
    Holder<Object> objectHolder = cachedInstances.get(name);
    if (Objects.isNull(objectHolder)) {
      cachedInstances.putIfAbsent(name, new Holder<>());
      objectHolder = cachedInstances.get(name);
    }
    Object value = objectHolder.getValue();
    if (Objects.isNull(value)) {
      synchronized (cachedInstances) {
        value = objectHolder.getValue();
        if (Objects.isNull(value)) {
          value = createExtension(name);
          objectHolder.setValue(value);
        }
      }
    }
    return (T) value;
  }

  /**
   * get all spi class spi.
   *
   * @return list. spi instances
   */
  public List<T> getSpiClassInstances() {
    Map<String, Class<?>> extensionClasses = this.getExtensionClasses();
    if (extensionClasses.isEmpty()) {
      return Collections.emptyList();
    }
    if (Objects.equals(extensionClasses.size(), cachedInstances.size())) {
      return (List<T>) this.cachedInstances.values().stream().map(e -> {
        return e.getValue();
      }).collect(Collectors.toList());
    }
    List<T> instances = new ArrayList<>();
    extensionClasses.forEach((name, v) -> {
      T instance = this.getSpiClassInstance(name);
      instances.add(instance);
    });
    return instances;
  }

  @SuppressWarnings("unchecked")
  private T createExtension(final String name) {
    Class<?> aClass = getExtensionClasses().get(name);
    if (Objects.isNull(aClass)) {
      throw new IllegalArgumentException("name is error");
    }
    Object o = spiClassInstances.get(aClass);
    if (Objects.isNull(o)) {
      try {
        spiClassInstances.putIfAbsent(aClass, aClass.newInstance());
        o = spiClassInstances.get(aClass);
      } catch (InstantiationException | IllegalAccessException e) {
        throw new IllegalStateException("Extension instance(name: " + name + ", class: "
          + aClass + ")  could not be instantiated: " + e.getMessage(), e);

      }
    }
    return (T) o;
  }

  /**
   * Gets extension classes.
   *
   * @return the extension classes
   */
  public Map<String, Class<?>> getExtensionClasses() {
    Map<String, Class<?>> classes = cachedClasses.getValue();
    if (Objects.isNull(classes)) {
      synchronized (cachedClasses) {
        classes = cachedClasses.getValue();
        if (Objects.isNull(classes)) {
          classes = loadExtensionClass();
          cachedClasses.setValue(classes);
        }
      }
    }
    return classes;
  }

  private Map<String, Class<?>> loadExtensionClass() {
    SPI annotation = clazz.getAnnotation(SPI.class);
    if (Objects.nonNull(annotation)) {
      String value = annotation.value();
      if (StringUtils.isNotBlank(value)) {
        cachedDefaultName = value;
      }
    }
    Map<String, Class<?>> classes = new HashMap<>(16);
    loadDirectory(classes);
    return classes;
  }

  private void loadDirectory(final Map<String, Class<?>> classes) {
    for (String directory : SPI_DIRECTORIES) {
      String fileName = directory + clazz.getName();
      try {
        Enumeration<URL> urls = Objects.nonNull(this.classLoader) ? classLoader.getResources(fileName)
          : ClassLoader.getSystemResources(fileName);
        if (Objects.nonNull(urls)) {
          while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            loadResources(classes, url);
          }
        }
      } catch (IOException t) {
        LOG.error("load extension class error {}", fileName, t);
      }
    }
  }

  private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {
    try (InputStream inputStream = url.openStream()) {
      Properties properties = new Properties();
      properties.load(inputStream);
      properties.forEach((k, v) -> {
        String name = (String) k;
        String classPath = (String) v;
        if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
          try {
            loadClass(classes, name, classPath);
          } catch (ClassNotFoundException e) {
            throw new IllegalStateException("load extension resources error", e);
          }
        }
      });
    } catch (IOException e) {
      throw new IllegalStateException("load extension resources error", e);
    }
  }

  private void loadClass(final Map<String, Class<?>> classes,
                         final String name, final String classPath) throws ClassNotFoundException {
    Class<?> subClass = Objects.nonNull(this.classLoader) ? Class.forName(classPath, true, this.classLoader) : Class.forName(classPath);
    if (!clazz.isAssignableFrom(subClass)) {
      throw new IllegalStateException("load extension resources error," + subClass + " subtype is not of " + clazz);
    }
    if (!subClass.isAnnotationPresent(Join.class)) {
      throw new IllegalStateException("load extension resources error," + subClass + " without @" + Join.class + " annotation");
    }
    Class<?> oldClass = classes.get(name);
    if (Objects.isNull(oldClass)) {
      classes.put(name, subClass);
    } else if (!Objects.equals(oldClass, subClass)) {
      throw new IllegalStateException("load extension resources error,Duplicate class " + clazz.getName() + " name " + name + " on " + oldClass.getName() + " or " + subClass.getName());
    }
  }

  /**
   * The type Holder.
   *
   * @param <T> the type parameter.
   */
  public static class Holder<T> {

    private volatile T value;

    /**
     * Gets value.
     *
     * @return the value
     */
    public T getValue() {
      return value;
    }

    /**
     * Sets value.
     *
     * @param value the value
     */
    public void setValue(final T value) {
      this.value = value;
    }
  }
}
